#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include "glad/glad.h" //放到GLFW/glfw3.h前面 GLAD的头文件包含了正确的OpenGL头文件(例如GL/gl.h)，所以需要在其他依赖于OpenGL的头文件之前引入GLAD
#include <GLFW/glfw3.h>

/**
 * 环境搭建:GLFW+GLAD
	基本编译库:
		sudo apt-get install build-essential

	OpenGL 库:
		sudo apt-get install libgl1-mesa-dev

	GLFW: https://www.glfw.org/download.html
		下载源码，运行 cmake
		正式编译：
			make
			sudo make install
	GLAD: https://glad.dav1d.de/
在这个网站上生成合适的库，选择 C/C++ OpenGL Core Version 4.6 然后点击 GENERATE 即可得到压缩包。把压缩包内 include 目录的东西放到本机 include 目录下(/usr/include),
 把 src 下的源码拷贝到工程内。
 *
 * openGL教程:https://learnopengl-cn.github.io/
 */

// settings
static const unsigned int SCR_WIDTH = 800;
static const unsigned int SCR_HEIGHT = 600;

//顶点着色器代码
const char *vertexShaderSource = "#version 460 core\n" //版本
                                 "layout (location = 0) \n"// 位置变量的属性位置值为0
                                 "in vec3 aPos;\n" //输入顶点
                                 "layout (location = 1)\n" // 颜色变量的属性位置值为 1
                                 "in vec3 aColor;\n" //输入颜色
                                 "out vec4 vertexColor;\n" // 为片段着色器指定一个颜色输出
                                 "out vec3 myColor;\n" // 向片段着色器输出一个颜色
                                 "void main()\n"
                                 "{\n"
                                 "   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n" //输出vec4类型的向量 把位置数据赋值给预定义的gl_Position变量
                                 "   vertexColor = vec4(0.5, 0.0, 0.0, 1.0);\n" // 把输出变量设置为暗红色
                                 "   myColor= aColor;" //设置为从顶点数据那里得到的输入颜色
                                 "}\0";
//片段着色器代码 片段着色器只需要一个输出变量，这个变量是一个4分量向量，它表示的是最终的输出颜色，我们应该自己将其计算出来。我们可以用out关键字声明输出变量，这里我们命名为FragColor
const char *fragmentShaderSource = "#version 460 core\n"
                                   "out vec4 FragColor;\n"
                                   "in vec4 vertexColor;\n"// 从顶点着色器传来的输入变量（名称相同、类型相同）
                                   "in vec3 myColor;\n"     // 从顶点着色器传来的输入变量（名称相同、类型相同）
                                   "uniform vec4 ourColor;\n"  // 在OpenGL程序代码中设定这个变量
                                   "void main()\n"
                                   "{\n"
                                //   "   FragColor = vertexColor;\n"
                                 //  "   FragColor = ourColor;\n"
                                   "   FragColor = vec4(myColor, 1.0);\n"
                                   "}\n\0";

//输入控制，检查用户是否按下了返回键(Esc)
static void processInput(GLFWwindow *window) {
    if(glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, GL_TRUE);
}
// 当用户改变窗口的大小的时候，视口也应该被调整
static void frame_buffer_size_callback(GLFWwindow* window, int width, int height) {
    // 注意：对于视网膜(Retina)显示屏，width和height都会明显比原输入值更高一点。
    glViewport(0, 0, width, height);
}

/**
 * 创建和链接着色器(顶点着色器,片段着色器)
 * 返回色器程序对象id
 */
static int createShader(){
    // build and compile our shader program
    // ------------------------------------
    // vertex shader 创建顶点着色器返回id
    int vertexShader = glCreateShader(GL_VERTEX_SHADER);
    //添加着色器源码 glShaderSource函数把要编译的着色器对象作为第一个参数。第二参数指定了传递的源码字符串数量，这里只有一个。第三个参数是顶点着色器真正的源码，第四个参数我们先设置为NULL
    glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
    //编译着色器
    glCompileShader(vertexShader);
    // check for shader compile errors
    int success;
    char infoLog[512];
    //检查是否编译成功
    glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
        printf("ERROR::SHADER::VERTEX::COMPILATION_FAILED:%s\n",infoLog);
    }
    // fragment shader 创建片段着色器 片段着色器所做的是计算像素最后的颜色输出
    int fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
    glCompileShader(fragmentShader);
    // check for shader compile errors
    glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog);
        printf("ERROR::SHADER::FRAGMENT::COMPILATION_FAILED:%s\n",infoLog);
    }
    // link shaders  创建着色器程序对象返回id 着色器程序对象(Shader Program Object)是多个着色器合并之后并最终链接完成的版本
    int shaderProgram = glCreateProgram();
    //添加着色器到着色器程序对象上 然后链接
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);

    // check for linking errors 检查是否链接错误
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
    if (!success) {
        glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
        printf("ERROR::SHADER::PROGRAM::LINKING_FAILED:%s\n",infoLog);
    }
    //删除着色器对象,链接完成后就不需要了
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);
    return shaderProgram;
}

/**
 * 创建顶点缓冲和索引缓冲对象并绑定数据
 */
static  void createBuffer(){
    //顶点数据----------------------------------------------------------------
    float vertices[] = {
            0.5f, 0.5f, 0.0f,   // 右上角
            1.0f, 0.0f, 0.0f,   //颜色

            0.5f, -0.5f, 0.0f,  // 右下角
            0.0f, 1.0f, 0.0f,   //颜色

            -0.5f, -0.5f, 0.0f, // 左下角
            0.0f, 0.0f, 1.0f,   //颜色

            -0.5f, 0.5f, 0.0f,   // 左上角
            0.0f, 1.0f, 0.0f    //颜色
    };
    //索引数据
    unsigned int indices[] = { // 注意索引从0开始!
            0, 1, 3, // 第一个三角形
            1, 2, 3  // 第二个三角形
    };
    unsigned int VBO, EBO; //顶点缓冲对象id,索引缓冲对象id
    glGenBuffers(1, &VBO); //创建一个缓冲对象,把id设置到VBO中
    glGenBuffers(1, &EBO); //创建一个缓冲对象,把id设置到EBO中
//顶点缓冲对象的缓冲类型是GL_ARRAY_BUFFER,使用glBindBuffer函数把新创建的缓冲绑定到GL_ARRAY_BUFFER目标上
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
//从这一刻起,使用的任何（在GL_ARRAY_BUFFER目标上的）缓冲调用都会用来配置当前绑定的缓冲(VBO)。然后我们可以调用glBufferData函数，它会把之前定义的顶点数据复制到缓冲的内存中
/**
 * glBufferData是一个专门用来把用户定义的数据复制到当前绑定缓冲的函数。它的
 第一个参数是目标缓冲的类型：顶点缓冲对象当前绑定到GL_ARRAY_BUFFER目标上。第
 二个参数指定传输数据的大小(以字节为单位)；用一个简单的sizeof计算出顶点数据大小就行。
 第三个参数是我们希望发送的实际数据。
第四个参数指定了我们希望显卡如何管理给定的数据。它有三种形式：
GL_STATIC_DRAW ：数据不会或几乎不会改变。
GL_DYNAMIC_DRAW：数据会被改变很多。
GL_STREAM_DRAW ：数据每次绘制时都会改变。
三角形的位置数据不会改变，每次渲染调用时都保持原样，所以它的使用类型最好是GL_STATIC_DRAW。如果，比如说一个缓冲中的数据将频繁被改变，那么使用的类型就是GL_DYNAMIC_DRAW或GL_STREAM_DRAW，这样就能确保显卡把数据放在能够高速写入的内存部分。
现在我们已经把顶点数据储存在显卡的内存中，用VBO这个顶点缓冲对象管理
 */
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    ////绑定顶点索引数据到缓冲区对象
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO); //索引缓冲对象
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
}

/**
 * 初始化
 */
static  void initWin(){
    glfwInit(); //初始化GLFW
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    //mac系统
#ifdef __APPLE__
    glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); // uncomment this statement to fix compilation on OS X
#endif
}
/**
 * hello world示例
 */
static  void helloOpenGL(){
    //初始化
    initWin();
    //创建窗口对象
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        printf("Failed to create GLFW window");
        glfwTerminate();
        exit(-1);
    }
    //通知GLFW将窗口的上下文设置为当前线程的主上下文
    glfwMakeContextCurrent(window);
    //对窗口注册一个回调函数,每当窗口改变大小，GLFW会调用这个函数并填充相应的参数供你处理
    glfwSetFramebufferSizeCallback(window, frame_buffer_size_callback);

    //初始化GLAD用来管理OpenGL的函数指针
    //GLFW给我们的是glfwGetProcAdress，它根据我们编译的系统定义了正确的函数
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        printf("Failed to initialize GLAD");
        exit(-1);
    }
    //在开始渲染之前必须告诉OpenGL渲染窗口(Viewport)的尺寸大小，这样OpenGL才能知道怎样根据窗口大小显示数据和坐标。
    glViewport(0, 0, 800, 600);

    //创建着色器程序
    int shaderProgram = createShader();

    //创建顶点数组对象
    unsigned int VAO; //顶点属性数组对象id
    glGenVertexArrays(1, &VAO); //创建顶点数组对象
    //绑定VAO
    //顶点数组对象(Vertex Array Object, VAO)可以像顶点缓冲对象那样被绑定，任何随后的顶点属性调用都会储存在这个VAO中
    glBindVertexArray(VAO);

    //创建缓冲对象
    createBuffer();

    /**
     * 设置顶点属性指针(位置):
        第一个参数指定我们要配置的顶点属性。还记得我们在顶点着色器中使用layout(location = 0)定义了position顶点属性的位置值(Location)吗？它可以把顶点属性的位置值设置为0。因为我们希望把数据传递到这一个顶点属性中，所以这里我们传入0。
        第二个参数指定顶点属性的大小。顶点属性是一个vec3，它由3个值组成，所以大小是3。
        第三个参数指定数据的类型，这里是GL_FLOAT(GLSL中vec*都是由浮点数值组成的)。
        第四个参数定义我们是否希望数据被标准化(Normalize)。如果我们设置为GL_TRUE，所有数据都会被映射到0（对于有符号型signed数据是-1）到1之间。我们把它设置为GL_FALSE。
        第五个参数叫做步长(Stride)，它告诉我们在连续的顶点属性组之间的间隔。由于下个组位置数据在3(6)个float之后，我们把步长设置为3(6) * sizeof(float)。要注意的是由于我们知道这个数组是紧密排列的（在两个顶点属性之间没有空隙）我们也可以设置为0来让OpenGL决定具体步长是多少（只有当数值是紧密排列时才可用）。一旦我们有更多的顶点属性，我们就必须更小心地定义每个顶点属性之间的间隔，我们在后面会看到更多的例子（译注: 这个参数的意思简单说就是从这个属性第二次出现的地方到整个数组0位置之间有多少字节）。
        最后一个参数的类型是void*，所以需要我们进行这个奇怪的强制类型转换。它表示位置数据在缓冲中起始位置的偏移量(Offset)。由于位置数据在数组的开头，所以这里是0。
     */
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
    //启用顶点属性
    glEnableVertexAttribArray(0);

    /**
     * 设置顶点属性指针(颜色):
     */
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3* sizeof(float)));
    //启用顶点属性
    glEnableVertexAttribArray(1);


    // note that this is allowed, the call to glVertexAttribPointer registered VBO as the vertex attribute's bound vertex buffer object so afterwards we can safely unbind
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    // You can unbind the VAO afterwards so other VAO calls won't accidentally modify this VAO, but this rarely happens. Modifying other
    // VAOs requires a call to glBindVertexArray anyways so we generally don't unbind VAOs (nor VBOs) when it's not directly necessary.
    glBindVertexArray(0);

    /**
     * 设置图形模式:
     */
    //glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); //线框模式
    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); //填充模式
    //渲染循环 使得GLFW在退出之前一直保持运行
    while(!glfwWindowShouldClose(window))
    {
        // 输入控制
        processInput(window);
        //渲染指令
        //当调用glClear函数，清除颜色缓冲之后，整个颜色缓冲都会被填充为glClearColor里所设置的颜色
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);
        float timeValue = glfwGetTime(); //获取运行的秒数
        float greenValue = (sin(timeValue) / 2.0f) + 0.5f; //按正弦曲线变化(周期函数 有循环效果)
        //查询uniform ourColor的位置值。我们为查询函数提供着色器程序和uniform的名字（这是我们希望获得的位置值的来源）。如果glGetUniformLocation返回-1就代表没有找到这个位置值
        int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");//ourColor:片段着色程序中的变量名
        glUseProgram(shaderProgram);
        //通过glUniform4f函数设置uniform值(设置ourColor的值),更新一个uniform之前你必须先使用程序（调用glUseProgram)
        glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f); //4f表示 4个float值
        //要想绘制我们想要的物体，OpenGL给我们提供了glDrawArrays函数，它使用当前激活的着色器，之前定义的顶点属性配置，和VBO的顶点数据（通过VAO间接绑定）来绘制图元
        glBindVertexArray(VAO); // seeing as we only have a single VAO there's no need to bind it every time, but we'll do so to keep things a bit more organized
       /**
        * glDrawArrays函数第一个参数是我们打算绘制的OpenGL图元的类型。由于我们在一开始时说过，我们希望绘制的是一个三角形，这里传递GL_TRIANGLES给它。第二个参数指定了顶点数组的起始索引，我们这里填0。最后一个参数指定我们打算绘制多少个顶点，这里是3（我们只从我们的数据中渲染一个三角形，它只有3个顶点长）。
        */
       // glDrawArrays(GL_TRIANGLES, 0, 3); //三角形

        /**
 第一个参数指定了我们绘制的模式，这个和glDrawArrays的一样。第二个参数是我们打算绘制顶点的个数，这里填6，也就是说我们一共需要绘制6个顶点。第三个参数是索引的类型，这里是GL_UNSIGNED_INT。最后一个参数里我们可以指定EBO中的偏移量（或者传递一个索引数组，但是这是当你不在使用索引缓冲对象的时候），但是我们会在这里填写0。
glDrawElements函数从当前绑定到GL_ELEMENT_ARRAY_BUFFER目标的EBO中获取索引。这意味着我们必须在每次要用索引渲染一个物体时绑定相应的EBO，这还是有点麻烦。不过顶点数组对象同样可以保存索引缓冲对象的绑定状态。VAO绑定时正在绑定的索引缓冲对象会被保存为VAO的元素缓冲对象。绑定VAO的同时也会自动绑定EBO。
         */
       glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); //矩形(两个三角形)

        //检查并调用事件，交换缓冲
        glfwSwapBuffers(window); //交换颜色缓冲
        //检查触发事件 函数检查是否有触发事件，比如键盘、鼠标等信号输入，然后更新窗口状态，调用相应的回调函数（可通过回调方法手动设置）。
        glfwPollEvents();
    }
    //释放/删除之前的分配的所有资源
    glfwTerminate();
}
int main_() {
    helloOpenGL();
    return 0;
}